反射一:基本类周边信息获取

相关文章:
Java Relfect
Java RelfectUtils
反射一:基本类周边信息获取
反射二:泛型相关周边信息获取
反射三:类内部信息获取

JAVA 反射机制是在运行状态中,对于任何一个类,都能够知道这个类的所有属性和方法;对于任何一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 JAVA 语言的反射机制。

一、引入


在开始反射之前,我们先看看 JVM 是如何将我们写的类对应的 java 文件加载到内存中的。

1、类的生命周期

这部分我们先讲讲 JVM 的加载机制。写一个最简单的 Main 函数,来看看这个函数的是如何被执行的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
animal.setName("cat");
}
// 内部静态类
public static class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

这段代码很简单,我们定义了一个 Animal 的类,在 main() 函数中,我们首先定义了一个 Animal 实例,然后调用了该实例的 setName() 方法。

大家都知道,在拿到一个 java 源文件后,如果要经过源码编译,要经过两个阶段:编译(javac.exe)、运行(java.exe)。

  • 编译
1
javac Test.java

在执行后在同一目录下生成 Test.class 和 Animal 类对应的文件Test$Animal.class(由于我们的 Animal 类是 Main 中的内部类,所以用 $ 表示)。

  • 运行
1
java Main

在这一阶段,又分为三个小阶段:装载,链接,初始化。

  • 装载
    类的装载是通过类加载器完成的,加载器将 .class 文件的二进制文件装入 JVM 的方法区,并且在堆区创建描述这个类的 java.lang.Class 对象,用来封装数据。 但是同一个类只会被类装载器装载一次!
  • 链接
    链接就是把二进制数据组装为可以运行的状态。链接分为校验、准备、解析这三个阶段。校验一般用来确认此二进制文件是否适合当前的 JVM(版本),准备就是为静态成员分配内存空间,并设置默认值。解析指的是转换常量池中的代码作为直接引用的过程,直到所有的符号引用都可以被运行程序使用(建立完整的对应关系)。
  • 初始化
    初始化就是对类中的变量进行初始化值。完成之后,类型也就完成了初始化,初始化之后类的对象就可以正常使用了,直到一个对象不再使用之后,将被垃圾回收,释放空间。

当没有任何引用指向 Class 对象时就会被卸载,结束类的生命周期。如果再次用到就再重新开始装载、链接和初始化的过程。

2、获取类类型

2.1、泛型隐藏填充类型

泛型隐藏填充类型默认填充为无界通配符?在上面,我们讲了,类只会被装载一次,利用装载的类可以实例化出各种不同的对象。而反射就是通过获取装载的类来做出各种操作的。装载的类,我们称为类类型,利用装载的类产生的实例,我们称为类实例。下面我们就看看,如何利用代码获取类类型的:

1
2
3
4
5
6
// 使用方法一
Class class1 = Animal.class;
Log.i(TAG, class1.getName());
// 使用方法二
Class<?> class2 = Animal.class;
Log.i(TAG, class2.getName());

运行结果如下:

从结果中可以看出 class1 和 class2 是完全一样的,那构造他们时的方法一和方法二有什么区别呢?

1
2
3
4
// 使用方法一
Class class1 = Animal.class;
// 使用方法二
Class<?> class2 = Animal.class;

可以看到这两个方法,右边全部都是 Animal.class,而左边却有些不同。方法一中,是直接生成了一个 Class 的实例。而在方法二中,则生成的是一个 Class 的泛型,并且使用的是无界通配符来填充的。我们都知道,Class 类是一个泛型,而泛型的正规写法就应该是

1
Class<Animal> class2 = Animal.class;

而方法一,只是把泛型的填充给省略了.在泛型中,如果把泛型的填充给省略掉,那就会默认填充为无界通配符”?”。所以方法一的真实写法是这样的:

1
Class<?> class1 = Animal.class;

所以这两种写法是意义是完全相同的。如果我们不用通配符,也就只能这样写:

1
Class<Animal> class2 = Animal.class;

2.2、获取类类型的方法

上面我们通过 Class<?> class1 = Animal.class; 即直接使用类名的 Class 对象来获取类类型,这只是其中一个方法,下面这四种方法都可以获得对应的类类型:

1
2
3
4
5
6
7
8
9
// 方法一:
Person person = new Person();
Class a = person.getClass()
// 方法二:
Class b = Persion.class;
// 方法三:
Class c = Class.forName(String className);
// 方法四:(不建议使用)
Class d = context.getClassLoader().loadClass(String className);

方法一:通过类实例的 getClass() 方法得到类类型;方法二:直接通过类的 class 对象得到;方法三和方法四中是通过类名得到,这两点要非常注意,这里的 className 一定要从包名具体到类名,唯一定位到一个类才行,不然就会报 ClassNotFound 错误。

在上面我们提到过,类只会被加载一次,所以 a、b、c、d 都是相等的,因为他们都是指向同一个对象,如果用等号操作符来判断的话:

1
boolean result = (clazz1 == clazz2 && clazz3 == clazz4 && clazz1 == clazz3);

result 的值为 true。

下面我们针对方法三和方法四举个粟子来看下:有一个单独的 Animal 类:

1
2
3
4
5
6
7
8
9
10
11
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestActivity extends BaseActivity {
private static final String TAG = "xian";
private Button btnCancel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_test);
btnCancel = findViewById(R.id.login_cancel_btn);
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
test();
} catch (Exception e) {}
}
});
}
private void test() throws Exception {
Class<?> class1 = Class.forName("com.xxt.xtest.Animal");
Log.d(TAG,"通过 Class.forName 获得的类名:" + class1.getName());
class1 = getClassLoader().loadClass("com.xxt.xtest.Animal");
Log.d(TAG,"通过 ClassLoader 获得的类名:"+class1.getName());
}
}

结果如下:

从上面的用法中,可以看出,我们要使用 Class.forName() 或者 getClassLoader().loadClass(),其中的类名必须是从包名到类名的完整路径。

从这里看来 Class.forName() 和 getClassLoader().loadClass() 是相同的,其实他们是有区别的。平时,我们不建议使用 getClassLoader().loadClass() 的方法来加载类类型。有关Class.forName() 和 getClassLoader().loadClass() 的具体区别,会在本篇末尾讲述。

二、基本类类型周边信息获取

我们知道类分为基本类和泛型类,这篇我们只讲基本类类型的周边信息获取,有关泛型类的周边信息获取,我们会放到下一篇中。
这部分主要讲述类类型周边信息获取方法,包括类名,包名,超类和继承接口。

1、类名、包名获取

相关的有三个函数:

1
2
3
4
5
6
// 获取完整的类名(包含包名)
public String getName();
// 仅获取类名
public String getSimpleName();
// 获取类类型所对应的 package 对象,几乎不用
public Package getPackage()

函数使用如下:

1
2
3
4
5
6
Class<?> class1 = Animal.class;
Package package1 = class1.getPackage();
Log.d(TAG,"完整的类名:" + class1.getName());
Log.d(TAG,"仅获取类名:" + class1.getSimpleName());
Log.d(TAG,"包名:" + package1.getName());

结果如下:

从结果中很清晰的看到,class.getName() 获取的是类名包含完整路径。调用 Class.forName() 就是用的这个值。class.getSimpleName() 得到的是仅仅是一个类名。而 class.getPackage() 得到的是该类对应的 Package 对象。通过 package.getName() 能获得该类所对应的包名。

2、获取超类 Class 对象

获取 superClass 的类对象,涉及到两个函数:

1
2
3
4
// 获取普通函数的父类 Class 对象
public Class<?> getSuperclass();
// 针对泛型父类而设计
public Type getGenericSuperclass();

getSuperclass() 用来获取普通函数,而 getGenericSuperclass() 是用来获取泛型类型的父类而设计的,有关 getGenericSuperclass() 的知识我们后面会讲,这里先看看 getSuperclass() 的用法。

我们仍然利用前面讲到的 Animal 类,然后在其上派生一个 AnimalImpl 子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class AnimalImpl extends Animal {
}

然后使用:

1
2
3
Class<?> class2 = Class.forName("com.xxt.xtest.AnimalImpl");
Class<?> parentClass = class2.getSuperclass();
Log.d(TAG, "父类:" + parentClass.getName());

结果如下:

在这里,我们使用了 Class.forName(“com.xxt.xtest.AnimalImpl”); 找到 AnimalImpl 的类类型对象,然后调用 class2.getSuperclass() 找到它的父类 Class 对象。很明显,它的父类是 Animal 类。由于我们这里得到了父类的 Class 对象 parentClass,所以可以对它使用 Class 的一切函数。所以调用 parentClass.getName() 就可以获得父类的名称了。

3、直接继承的接口的 Class 对象

这里要先声明一个观点:Class 类,不同于定义类的 class 标识,Class 类是一个泛型。类对象是由 Class 对象来表示,而接口对象同样也是用 Class 对象来表示。所以同样是 Class 对象,它可能表示的类对象,也可能表示的是接口对象。获取接口对象的函数如下:

1
2
3
4
// 获取普通接口的方法
public Class<?>[] getInterfaces();
// 获取泛型接口的方法
public Type[] getGenericInterfaces();

与获取 superClass 对象一样,这里同样有两个函数来获取接口对象,有关 getGenericInterfaces() 获取泛型接口的方法,我们下篇再讲,这里先讲讲获取普通接口的方法 getInterfaces()。

getInterfaces() 将获取指定类直接继承的接口列表,注意是直接继承。如果不是直接继承,那将是获取不到的。举个例子,以上面的 Animal 为例:

我们先声明一个接口,让 Animal 类来继承:

1
2
3
4
public interface IAnimal {
void setName(String name);
String getName();
}

然后是 Animal 类继承接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Animal implements IAnimal {
private String name;
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
}

为了测试不是直接继承的接口是无法获取的问题,我们再从 Animal 派生一个子类 AnimalImpl:

1
2
public class AnimalImpl extends Animal {
}

我们再整理一下思路,Animal 类直接继承了 IAnimal,而 AnimalImpl 仅仅派生自 Animal,它的 IAnimal 接口不是直接继承的,而是从它的父类 Aniaml 那带过来的。

然后我们分别看看 Animal 类和 AnimalImpl 类的的获取接口的结果,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取 Animal 类的接口列表
Class<?> class3 = Animal.class;
Class<?>[] interfaces = class3.getInterfaces();
for (Class interItem:interfaces){
Log.d(TAG, "Animal 继承的接口:" + interItem.getName());
}
// 获取 AnimalImpl 的接口列表
class3 = AnimalImpl.class;
interfaces = class3.getInterfaces();
if (interfaces.length >0) {
for (Class interItem : interfaces) {
Log.d(TAG, "AnimalImpl 继承的接口:" + interItem.getName());
}
} else {
Log.d(TAG, "AnimalImpl 无继承的接口");
}

结果如下:

从结果可以看出,这里找到了 Animal 类所继承的接口值;但 AnimalImpl 获取到的接口列表为空。所以这也证明了 getInterfaces() 只能获取类直接继承的接口列表。

4、获取某个类类型的所有接口

如果我想传进去一下类类型,然后要得到它所有继承的接口列表要怎么办?不管它是不是直接继承来的都要列出来。

那只有靠递规了,我们需要递规它的父类直接继承的接口、父类的父类直接继承的接口以此类推,最终到 Object 类的时候就找到所有继承的接口了。

在开始递规获取所有接口之前,我们先构造下代码。由于我们要获取所有接口,为了效果更好些,我们在 Animal 和 AnimalImpl 基础上,多加几个继承的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 给 Animal 添加 IAnimal、Serializable 两个接口
public class Animal implements IAnimal, Serializable {
private String name;
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
}
// 给 AnimalImpl 添加 Serializable 接口
public class AnimalImpl extends Animal implements Serializable {
}

所以如果我们获取 AnimalImpl 类的接口列表,得到的应该是三个:自已直接继承的 Serializable、从父类 Animal 那继承的 IAnimal 和 Serializable。获取类类型所有接口列表的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 获取所传类类型的所有继承的接口列表
* @param clazz
* @return
*/
public Class<?>[] getAllInterface(Class<?> clazz){
// 获取自身的所有接口
Class<?>[] interSelf = clazz.getInterfaces();
// 递规调用getAllInterface获取超类的所有接口
Class<?> superClazz = clazz.getSuperclass();
Class<?>[] interParent = null;
if (null != superClazz) {
interParent = getAllInterface(superClazz);
}
// 返回值
if (interParent == null && interSelf != null) {
return interSelf;
} else if (interParent == null && interSelf == null){
return null;
} else if (interParent != null && interSelf == null){
return interParent;
} else {
final int length = interParent.length + interSelf.length;
Class<?>[] result = new Class[length];
System.arraycopy(interSelf,0,result,0,interSelf.length);
System.arraycopy(interParent,0,result,interSelf.length,interParent.length);
return result;
}
}

测试代码:

1
2
3
4
5
6
7
Class<?>[] clazzes = getAllInterface(AnimalImpl.class);
SpannableStringBuilder builder = new SpannableStringBuilder();
for (Class clazz : clazzes) {
builder.append(clazz.getName());
builder.append(" ");
}
Log.d(TAG, "AnimalImpl 继承的所有接口: " + builder.toString());

先看看执行结果:

这段代码最关键的地方在于 getAllInterface(Class<?> clazz);代码分为两部分,第一部分是获得自己的接口列表和父类的列表:通过 Class<?>[] interSelf = clazz.getInterfaces(); 获得自已直接继承的接口列表。然后,通过 Class<?> superClazz = clazz.getSuperclass(); 获取父类的 Class 类型,然后调用 getAllInterface(superClazz) 获得父类的所有接口列表。那么,把它们两个合并,就是所有的接口列表了。

合并逻辑:对 interParent 和 interSelf 判空,如果两个列表都是空,那直接返回空;如果有一个是空,另一个不是空,则返回那个不是空的列表;如果两个都不是空,则将他们合并,然后返回合并后的列表。

5、获取类的访问修饰符

由于我们在定义类时,比如下面的内部类:

1
2
public static final class InnerClass {
}

在类名,前面的那一坨 public static final,就是类的访问修饰符,是定义这个类在的访问区域和访问限定的。这部分就讲讲如何获取类的这部分访问修饰符,以上面的内部类 InnerClass 为例:

1
2
3
4
5
6
Class<?> clazz = getClassLoader().loadClass(InnerClass.class.getName());
int modifiers = clazz.getModifiers();
String retval = Modifier.toString(modifiers);
boolean isFinal = Modifier.isFinal(modifiers);
Log.d(TAG, "InnerClass 的定义修饰符: " + retval);
Log.d(TAG, "is Final: " + isFinal);

结果如下:

首先,在这部分代码中,我们又换了一种类加载方式,使用的是 ClassLoader。然后我们单独来看看这句:

1
int modifiers = clazz.getModifiers();

通过 clazz.getModifiers() 得到一个整型变量,由于访问修饰符有很多,所以这些修饰符被打包成一个 int,对应的二进制中,每个修饰符是一个标志位,可以被置位或清零。另外 Java 开发人员单独提供了一个类来提取这个整型变量中各标识位的函数,这个类就是 Modifier。Modifier 中主要有以下几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据整型变量来生成对应的修饰符字符串
String Modifier.toString(int modifiers)
// 以下这些方法来检查特定的修饰符是否存在
boolean Modifier.isAbstract(int modifiers)
boolean Modifier.isFinal(int modifiers)
boolean Modifier.isInterface(int modifiers)
boolean Modifier.isNative(int modifiers)
boolean Modifier.isPrivate(int modifiers)
boolean Modifier.isProtected(int modifiers)
boolean Modifier.isPublic(int modifiers)
boolean Modifier.isStatic(int modifiers)
boolean Modifier.isStrict(int modifiers)
boolean Modifier.isSynchronized(int modifiers)
boolean Modifier.isTransient(int modifiers)
boolean Modifier.isVolatile(int modifiers)

首先是 toString 函数:这个函数的作用就是根据传进来的整型,获取其中的标识位来判断具有哪个修饰符,然后将所有修饰符拼接起来输出。比如我们的例子中输出的就是:public static final。

其它的就是一些 isXXXX(int moifiers) 的判断指定标识位的函数了。在例子中,我们使用了 Modifier.isFinal(int modifiers) 来判断是不是具有 final 修饰符,返回结果为 true。

6、获取接口的访问修饰符

从上面获取类的访问修饰符时,接口、类、函数都是通过 Modifier 类判断访问修饰符的,又因为类和接口类型全部都是用 Class 对象来标识,所以接口和类的获取访问修饰符的方式完全相同,下面就举一个简单的例子:

1
2
3
// 定义一个类部接口
public static interface InnerInteface {
}

测试代码:

1
2
3
4
5
6
Class<?> clazz2 = InnerInteface.class;
int modifiers = clazz2.getModifiers();
String str = Modifier.toString(modifiers);
boolean isInterface = Modifier.isInterface(modifiers);
Log.d(TAG, "InnerClass 的定义修饰符: " + str);
Log.d(TAG, "isInterface: " + isInterface);

如果我们要直接获取一个接口的对象,同样,也是通过开头所讲的那四种获取Class对象的方式。因为我们现在知道 Class 对象,不光代表类也可以代表接口。有关 Modifier 的使用与第五部分获取类的修饰符是一样的。

7、Class.forName 与 ClassLoader.loadClass 的区别

我们通过源码来看看他们的区别。先看 Class.forName:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Class<?> forName(String className) throws ClassNotFoundException {
return forName(className, true, VMStack.getCallingClassLoader());
}
public static Class<?> forName(String className, boolean initializeBoolean,
ClassLoader classLoader) throws ClassNotFoundException {
Class<?> result;
try {
result = classForName(className, initializeBoolean, classLoader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof ExceptionInInitializerError) {
throw (ExceptionInInitializerError) cause;
}
throw e;
}
return result;
}

从源中可以看到 Class.forName(String className) 最终调用的是 forName(String className, boolean initializeBoolean, ClassLoader classLoader) 函数。
其中:

  • className:类名。
  • initializeBoolean:表示是否需要初始化;如果设为 true,表示在加载以后,还会进入链接阶段。
  • classLoader:ClassLoader 加载器。

我们知道源文件在编译后,在运行时,分为三个阶段:加载、链接和初始化。这里的 initializeBoolean 就是定义是否进行链接和初始化。而 Class.forName 默认是设置的为 true。所以利用 Class.forName() 得到的类类型,除了加载进来以外,还进行了链接和初始化操作。

下面再来看看 ClassLoader.loadClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
if (clazz == null) {
clazz = findClass(className);
}
}
return clazz;
}

loadClass(String className) 最终是调用递规函数 loadClass(String className, boolean resolve) 来将类加载出来。通过代码也可以看出来 ClassLoader 的 loadClass(String className) 只是将类加载出来,并没有链接与初始化的步骤。

最后,我们总结一下,Class.forName(String className) 不仅会将类加载进来,而且会对其进行初始化,而 ClassLoader.loadClass(String ClassName) 则只是将类加载进来,而没有对类进行初始化。一般来讲,他们两个是通用的,但如果你加载类依赖初始化值的话,那 ClassLoader.loadClass(String ClassName) 将不再适用。

举例来说:在 JDBC 编程中,常看到这样的用法。Class.forName(“com.mysql.jdbc.Driver”); 如果换成了 getClass().getClassLoader().loadClass(“com.mysql.jdbc.Driver”); 就不行。

为什么呢?打开 com.mysql.jdbc.Driver 的源代码看看:

1
2
3
4
5
6
7
8
// Register ourselves with the DriverManager
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

原来,Driver 在 static 块中会注册自己到 java.sql.DriverManager。而 static 块就是在 Class 的初始化中被执行。所以这个地方就只能用Class.forName(className)。

这篇文章所涉及到的几个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 获取类类型对象的几种方式:
Person person = new Person();
Class a = person.getClass() // 方法一
Class b = Persion.class; // 方法二
Class c = Class.forName(String ClassName); // 方法三
Class d = context.getClassLoader().loadClass(String ClassName); // 方法四(不建议使用)
// 获取包名类名
public String getName(); // 获取完整的类名(包含包名)
public String getSimpleName(); // 仅获取类名
public Package getPackage(); // 获取类类型所对应的 package 对象
// 获取超类 Class 对象
public Class<?> getSuperclass(); // 获取普通函数的父类 Class 对象
public Type getGenericSuperclass(); // 针对泛型父类而设计(下篇讲解)
// 获取接口 Class 对象
public Class<?>[] getInterfaces(); // 获取普通接口的方法
public Type[] getGenericInterfaces(); // 获取泛型接口的方法
// 类访问修饰符
int modifiers = clazz.getModifiers(); // 获取类访问修饰符对应的 int 变量
String Modifier.toString(int modifiers); // 根据整型变量来生成对应的修饰符字符串
boolean Modifier.isAbstract(int modifiers); // isXXX() 系列函数用以检查特定的修饰符是否存在

原文链接:
夯实JAVA基本之二 —— 反射(1):基本类周边信息获取